DAL 的职责是:
封装所有数据访问和数据相关的业务规则,为上层提供统一的数据接口。
例如:
xxxxxxxxxxPage (Server Component)│▼Data Access Layer│▼Prisma / PostgreSQL
而不是:
xxxxxxxxxxPage│▼Prisma
假设你的项目是这样的:
xxxxxxxxxxapp/├── dashboard/page.tsx├── users/page.tsxlib/├── db.ts├── auth.ts└── data/├── users.ts├── posts.ts└── orders.ts
真正的 DAL 通常在:
xxxxxxxxxxlib/data/
例如:
// lib/data/users.tsimport { prisma } from "@/lib/db";export async function getUserById(id: string) { return prisma.user.findUnique({ where: { id }, });}export async function getUsers(companyId: string) { return prisma.user.findMany({ where: { companyId, deleted: false, }, });}这一整个文件:
xxxxxxxxxxusers.ts
就是:
Users Data Access Layer
不是里面某一个函数。
其余的posts.ts,orders.ts同样是DAL。
What is a Data Access Layer?
A Data Access Layer is the application's single entry point for accessing data. Instead of letting pages or Server Actions talk to Prisma directly, all data-related concerns—such as database queries, authentication, authorization, caching, error handling, and business rules—are encapsulated behind reusable data access functions like
getUsers()orcreateOrder().
例如在多个页面可能都会写这段代码,如果有修改呢?岂不是要改很多地方。
// app/dashboard/page.tsxconst users = await prisma.user.findMany({ where: { companyId: session.user.companyId, deleted: false, },});DAL:
// lib/data/users.tsexport async function getUsers(companyId: string) { return prisma.user.findMany({ where: { companyId, deleted: false, status: "ACTIVE", }, });}以后:
const users = await getUsers(companyId);统一维护。
这是 Next.js 官方最推荐 DAL 的原因。
不推荐在页面里面写权限控制代码:
// page.tsxconst session = await auth();const orders = await prisma.order.findMany();任何页面都可能忘记权限。
DAL:
export async function getOrders() { const session = await auth(); if (!session) { throw new Error("Unauthorized"); } return prisma.order.findMany({ where: { companyId: session.user.companyId, }, });}以后所有地方:
await getOrders();权限永远一致。
例如:
export default async function Page() { const users = await getUsers(); return <UserTable users={users} />;}Page 根本不知道:
它只知道:
const users = await getUsers();符合:
xxxxxxxxxxHigh CohesionLow Coupling
例如:
export async function getUsers() { "use cache"; return prisma.user.findMany();}如果以后:
需要:
只改 DAL:
import { cacheTag, cacheLife } from "next/cache";export async function getUsers() { "use cache"; cacheTag("users"); cacheLife("minutes"); return prisma.user.findMany();}Page:
完全不用改。
数据更新:
"use server";import { revalidateTag } from "next/cache";export async function createUserAction(data) { await prisma.user.create({ data, }); revalidateTag("users");}整个缓存体系:
xxxxxxxxxxServer Component↓getUsers()↓use cache↓cacheTag("users")↓Database
更新:
xxxxxxxxxxServer Action↓createUser()↓revalidateTag("users")↓下一次重新查询
缓存策略集中管理,不会散落在每个页面里。
Page:
const users = await getUsers();测试:
Mock:
vi.mock("./users");而不是 Mock:
prisma.user.findMany()For medium to large Next.js projects, I usually introduce a Data Access Layer. Instead of letting Server Components or Server Actions access Prisma directly, I encapsulate database queries, authorization checks, and business rules into reusable functions. This improves code reuse, centralizes permission logic, makes caching easier to introduce, and keeps the presentation layer focused on rendering rather than data access.
中文:
对于中大型 Next.js 项目,我通常会引入 Data Access Layer,而不是让 Server Components 或 Server Actions 直接访问 Prisma。DAL 负责封装数据库查询、权限校验以及部分业务规则,这样可以提高代码复用率、统一权限控制、方便后续增加缓存,并让页面组件专注于 UI 渲染。
它的两个文件helpers.ts和types.ts,其实不是完整的 DAL,更准确地说,它是 DAL 的基础设施(DAL Infrastructure) 或者 DAL Utilities。真正的DAL是我的data.ts这个文件,也可以根据featuer来区分多个文件,这多个文件就是DAL。
xxxxxxxxxxdalRequireAuth()dalDbOperation()dalVerifySuccess()它们其实没有查询数据库。
例如:
xxxxxxxxxxexport async function dalRequireAuth()它做的是:
xxxxxxxxxxAuthentication不是:
xxxxxxxxxxDatabase Access再看:
xxxxxxxxxxdalDbOperation()它做的是:
xxxxxxxxxxtry↓catch↓Prisma Error↓包装成 DalReturn它也没有:
xxxxxxxxxxprisma.user.findMany()所以它其实更像:
xxxxxxxxxxDAL Helper或者:
xxxxxxxxxxDAL Base Functions如果按照他的架构。
真正查询用户可能是:
xxxxxxxxxx// lib/data/users.tsimport { dalDbOperation, dalRequireAuth } from "./dal";export async function getUsers() { return dalRequireAuth(async (session) => { return dalDbOperation(async () => { return prisma.user.findMany({ where: { companyId: session.user.companyId, }, }); }); });}这里:
xxxxxxxxxxgetUsers()才是真正的:
xxxxxxxxxxUsers DAL里面用了:
xxxxxxxxxxdalRequireAuth()↓dalDbOperation()这些 helper。
所以:
xxxxxxxxxxDAL和:
xxxxxxxxxxDAL Utilities不是一个东西。
因为如果:
xxxxxxxxxxgetUsers()getOrders()getDashboard()getProducts()createPost()都需要:
xxxxxxxxxxconst session = await auth();以及:
xxxxxxxxxxtry {}catch {}你难道复制:
xxxxxxxxxxtrycatch二十遍?
当然不要。所以抽:
xxxxxxxxxxdalDbOperation()以后,所有 DAL共享。
我一般会分三层:
x
lib/data/│├── dal.ts <-- DAL 基础设施│├── users.ts <-- Users DAL│├── posts.ts <-- Posts DAL│├── orders.ts <-- Orders DALxxxxxxxxxxdal.ts负责:
xxxxxxxxxxAuth↓Error↓Cache↓Transaction↓Permission例如:
xxxxxxxxxxdalRequireAuth()dalDbOperation()dalCache()dalThrowError()xxxxxxxxxxusers.ts真正的数据访问:
xxxxxxxxxxexport async function getUsers() {}export async function getUserById() {}export async function createUser() {}里面调用:
xxxxxxxxxxdalRequireAuth()↓dalDbOperation()Page:
xxxxxxxxxxconst users = await getUsers();根本不知道:
xxxxxxxxxxAuth↓Prisma↓Cache↓Error因为他的设计理念是:
DAL 不仅负责访问数据库,还负责数据访问过程中所有横切关注点(Cross-Cutting Concerns)。
这些包括:
DalReturn)use cache、cacheTag)所以:
xxxxxxxxxxDAL Infrastructure↓Users DAL↓Orders DAL↓Products DAL这套设计实际上分成了两层:
负责所有横切关注点(Cross-Cutting Concerns):
dalRequireAuth() —— 登录校验dalDbOperation() —— 数据库异常处理dalVerifySuccess() —— 简化调用dalLoginRedirect() —— 自动跳转登录页DalReturn —— 统一返回协议DalError —— 统一错误协议这些函数本身不关心业务,可以被所有数据访问函数复用。
例如:
xxxxxxxxxxgetUsers()getUserById()createUser()deleteUser()getOrders()
这些函数负责真正访问数据库,它们组合使用第一层提供的公共能力,最终形成整个应用唯一的数据访问入口。
这也是 Web Dev Simplified 这套 DAL 架构的核心思想:业务代码只负责业务,认证、错误处理、返回格式等公共逻辑全部下沉到 DAL Infrastructure 中统一管理。
其实DAL最难理解的就是,函数里面嵌套函数,将函数抽象为参数传递到函数中,并且函数参数里面还有参数,这是我最陌生的地方。只要多接触,自然就熟悉了。
如果只是单纯的函数作为参数传递进来,我还是可以理解的:
xxxxxxxxxxexport async function dalRequireAuthOperation(operation: () => void) { const session = await auth.api.getSession({ headers: await headers(), }); if (session) { return operation(); }}但如果函数作为参数,并且函数参数里面定义有参数和返回值类型,我看上去就很难理解了:
xxxxxxxxxxexport async function dalRequireAuth<T>( operation: (session: SessionType) => Promise<DalReturn<T>>,) { const session = await auth.api.getSession({ headers: await headers(), // you need to pass the headers object. }); if (!session) { return createErrorReturn({ type: "no-user", }); } return operation(session);}其实仔细拆解一下,就会发现,operation就是一个函数,并且在调用operation函数的时候,可以接收到传递过来的session参数,就是这里难以理解。这就是闭包的概念。但是这里非常抽象,就是operation其实我不知道是什么样的,
参考webdevsimplied这个老师的做法。这个DAL是每个项目都必须的,必须把这个功能加上。
老师的项目使用了sqlite3,所以不需要专门的数据库,使用npm install,然后运行drizzle相关的命令即可。重点关注两个页面:/todos和/admin。
有_naive、_dal-basic、_dal-advanced三种类型的文件,分别对应的是不同的data access layer写法,可以将其替换到page.tsx里面看效果。

action函数也有data access layer,也是分为三种类型,可以对比看。

naive里面的代码就不要参考了,这是不推荐的写法,但是dal-advanced里面的封装太复杂了,这个暂时还看不懂,所以直接参考dal-basic的写法,能够写成这样就已经很不错了。dal-advanced里面的代码要慢慢学会写。
naive的写法就是在页面中直接进行用户权限检查和数据库查询,这样会导致大量的样板代码,如果直接在页面查询,很容易忘记添加权限检查,从而导致安全漏洞(例如普通用户访问了管理员数据)。反之将数据获取逻辑封装在函数中,可以更方便地统一应用 Next.js 的缓存机制(如 cache 或 revalidateTag)。
下面这样的代码就不要写了:

最起码要写成这样的,要先封装一个函数,然后在页面中调用(也就相当于把检查用户是否登录的语句放到封装函数里面了):


仔细了解一下之后,好像dal-basic也太简单了一点,dal-basic的mutations和queries里面对user的校验代码也很重复,而且还是手动抛出错误,但就是简单,别人一看代码就明白了。
dal-advanced的代码封装,太抽象了一点,我看了很久,很难总结出它到底做了什么,我问一下AI:
在视频中,Kyle 介绍的
dal-advanced(高级数据访问层)模式,核心思想是将数据获取逻辑从“直接抛出错误/重定向”转变为“返回结构化的结果对象”。这种模式模仿了类似 Rust 的
Result类型或验证库(如 Zod)的返回结构。以下是该模式代码设计的详细拆解:
- 核心数据结构:Result 对象
在
dal-advanced中,DAL 函数不再直接返回数据,而是返回一个包含状态的对象。其 TypeScript 定义大致如下:x// 这里的extends的意思是,传过来的E的类型,必须长得有DalError这样子。export type DalReturn<T, E extends DalError = DalError> =| {success: truedata: T}| {success: falseerror: E}export type DalError =| {type: "no-user"}| {type: "no-access"}| {type: "drizzle-error"error: DrizzleQueryError}| {type: "unknown-error"error: unknown}为什么要这么做?
- 可预测性:调用者通过检查
success属性,就能明确知道操作是否成功,而不需要用try/catch去捕获可能发生的重定向或错误。- 逻辑解耦:DAL 层只负责检查权限和拿数据,不决定出错后该怎么办(是跳转页面、弹窗还是返回 401 状态码)。
- 标准化的 DAL 函数写法
一个高级 DAL 函数(如获取用户待办事项)的代码结构通常如下:
xxxxxxxxxxexport async function getTodos() {const user = await getCurrentUser(); // 获取当前用户// 1. 权限检查:不直接 redirect,而是返回错误类型if (!user) {return { success: false, error: 'NOT_AUTHENTICATED' } as const;}try {// 2. 数据库查询const data = await db.query.todos.findMany({where: eq(todos.userId, user.id)});// 3. 成功返回return { success: true, data };} catch (e) {// 4. 异常处理return { success: false, error: 'UNKNOWN_ERROR' } as const;}}
- 调用层的灵活处理(分层消费)
这是
dal-advanced最强大的地方,不同的调用方可以根据返回的error做出不同反应:场景 A:在 React Server Component (页面) 中
如果你希望用户没登录就直接跳到登录页,可以使用一个辅助函数
verifyData:xxxxxxxxxx// 页面组件export default async function TodosPage() {const result = await getTodos();// 辅助函数:如果 success 为 false,内部执行 redirect('/login')const data = verifyData(result);return <div>{/* 渲染 data */}</div>;}场景 B:在 API Route 或 Server Action 中
如果你正在处理一个异步请求,不希望页面跳转,而是想给前端一个 JSON 错误:
xxxxxxxxxxexport async function POST() {const result = await getTodos();if (!result.success) {if (result.error === 'NOT_AUTHENTICATED') {return new Response("请先登录", { status: 401 });}return new Response("出错了", { status: 500 });}return Response.json(result.data);}
- 进阶:使用 Wrapper 减少冗余
视频中还提到了创建一个通用的包装器(如
dbWrapper),自动处理数据库错误捕获:xxxxxxxxxxasync function dbOperation<T>(operation: () => Promise<T>): Promise<DALResult<T>> {try {const data = await operation();return { success: true, data };} catch (error) {console.error(error);return { success: false, error: 'UNKNOWN_ERROR' };}}总结
dal-advanced的代码优势:
- 强类型支持:通过 TypeScript,你在处理
result.data之前必须先判断result.success,减少了空指针崩溃。- 错误分类:将“未登录”、“无权限”和“数据库崩溃”明确区分开。
- 多环境复用:同一套 DAL 代码可以无缝用于页面渲染、后台接口和自动化脚本,因为它的逻辑是“纯粹”的返回,不依赖于 Next.js 的路由环境。
这种模式虽然增加了一些样板代码(每个函数都要返回对象),但在中大型项目中,它带来的可测试性和安全性远超基础模式。
这不就是我一直寻找的问题答案吗?我之前一直担心nextjs里面的数据操作,怎么处理错误?要不要写try...catch?写了try...catch之后,boilerplate代码太多了,显示不出我的能力啊。这个dal-advanced不就解决了问题嘛。
多看几遍,一遍肯定是看不懂的。看懂了之后,自己写几遍。
这个文件里面是一套封装函数,这段代码本质上是一套工具集,目的是让你的 Next.js 应用在处理数据库请求时,既安全又灵活。
核心原理是:将数据操作变成一个可以被层层拦截、层层处理的“管道”。
我们可以把这些函数拆分为 执行者 - 处理数据库(dalDbOperation)、动作适配器 - 处理结果(Redirect/Throw/VerifySuccess)、守门员/保镖(RequireAuth)和辅助工具(Format)。
dalDbOperation<T>(operation)
xxxxxxxxxxexport async function dalDbOperation<T>(operation: () => Promise<T>) { try { // 先执行operation函数,如果正常,那么就返回data const data = await operation(); return createSuccessReturn(data); } catch (e) { // 这里的e就是错误,返回不同的错误即可 if (e instanceof ThrowableDalError) { return createErrorReturn(e.dalError); } if (e instanceof DrizzleQueryError) { return createErrorReturn({ type: "drizzle-error", error: e }); } return createErrorReturn({ type: "unknown-error", error: e }); }}这是所有数据库操作的“防护壳”。
try/catch 紧紧包裹住数据库操作。如果是数据库报错,它会把它包装成一个友好的错误对象返回( { success: false, error: ... }),而不是让整个页面崩掉(White Screen of Death)。dalRequireAuth<T>(operation, options)
xxxxxxxxxxexport async function dalRequireAuth<T, E extends DalError>( operation: (user: typeof UserTable.$inferSelect) => Promise<DalReturn<T, E>>, { allowedRoles }: { allowedRoles?: UserRole[] } = {},) { const user = await getCurrentUser(); if (user == null) { return createErrorReturn({ type: "no-user" }); } if (allowedRoles && !allowedRoles.includes(user.role)) { return createErrorReturn({ type: "no-access" }); } return operation(user);}这是最核心的函数,用于定义一个受保护的数据操作。
写法:它接收你真正想做的操作(operation函数参数),以及允许访问的角色列表allowedRoles(是一个对象里面的数组,为什么不直接传递数组,可能原因是这里可以直接传递session数据,这个数据里面有allowedRoles数组,更加方便了)。
作用:
getCurrentUser)。no-user 错误,不执行数据库查询。no-access 错误。价值:它把“鉴权”和“取数”解耦了,你的数据库查询代码里不需要再写一遍权限检查。你再也不用在每个数据库函数里重复写 if (user == null) ... 了。
这组函数决定了当 DAL 返回结果后,程序该表现出什么行为。
dalLoginRedirect(dalReturn)
xxxxxxxxxxexport function dalLoginRedirect<T, E extends DalError>( dalReturn: DalReturn<T, E>,) { if (dalReturn.success) return dalReturn; if (dalReturn.error.type === "no-user") return redirect("/login"); return dalReturn as DalReturn<T, Exclude<E, { type: "no-user" }>>;}no-user 错误,直接调用 Next.js 的 redirect("/login")。Exclude方法。如果函数执行完没跳转,TS 会自动推断出剩下的结果里一定不会再有“未登录”这个错误类型。dalUnauthorizedRedirect(dalReturn, path)
xxxxxxxxxxexport function dalUnauthorizedRedirect<T, E extends DalError>( dalReturn: DalReturn<T, E>, redirectPath = "/",) { if (dalReturn.success) return dalReturn; if (dalReturn.error.type === "no-access") return redirect(redirectPath); return dalReturn as DalReturn<T, Exclude<E, { type: "no-access" }>>;}no-access(没权限),直接跳转到首页(或你指定的路径)。dalThrowError(dalReturn)
xxxxxxxxxxexport function dalThrowError<T, E extends DalError>( dalReturn: DalReturn<T, E>,) { if (dalReturn.success) return dalReturn; throw dalReturn.error;}error.js 错误页面。dalVerifySuccess(dalReturn, options) —— 终极聚合函数
xxxxxxxxxxexport function dalVerifySuccess<T, E extends DalError>( dalReturn: DalReturn<T, E>, { unauthorizedRedirectPath }: { unauthorizedRedirectPath?: string } = {},): T { const res = dalThrowError( dalUnauthorizedRedirect( dalLoginRedirect(dalReturn), unauthorizedRedirectPath, ), ); return res.data;}这是你在 Server Component(页面文件)里最常用的函数。
dalReturn 检查登录 检查权限 检查其他错误 成功则返回 data。const data = dalVerifySuccess(await getMyData())。如果有任何问题,用户会自动被送走;如果有数据,你就直接拿到了 data,不需要再写 if 判断。这是一个链式组合。它依次检查:是否没登录?(是就跳登录页) -> 是否没权限?(是就跳首页或其它页) -> 是否有其他错误?(是就直接抛异常)。
dalFormatErrorMessage(error)no-user)翻译成人类能看懂的话(“您需要登录才能执行此操作”)。为了帮你理清思路,请看下面这个逻辑流向图:
完整的使用流程如下:
定义层 (Data Layer):
使用 dalRequireAuth 包装你的 SQL 查询,确保用户登录后才能操作SQL。使用dalDbOperation包裹数据库操作,使用try...catch捕获错误,将返回结果或者错误包装成对象返回。
处理层 (Action/Page):
dalVerifySuccess:遇到错误自动重定向,成功直接拿数据。result.success:如果失败,用 dalFormatErrorMessage 拿到文字发回给前端显示。这样写的最大好处: 你的业务逻辑(SQL)只需要写一次,但在“直接跳页面”和“只显示报错文字”两个场景下都能复用。
- 逻辑流示意图
为了让你理解这些函数是怎么串联的,看这个流程:
① UI 发起请求 调用一个 DAL 函数。
② DAL 函数内部 调用
dalRequireAuth。③
dalRequireAuth检查权限 失败则返回错误对象 成功则调用dalDbOperation。④
dalDbOperation执行 SQL 成功则返回{ success: true, data }。⑤ UI 拿到结果 调用
dalVerifySuccess。
- 如果是错误对象 自动触发
redirect。- 如果是成功对象 拿到
data开始渲染。
- 为什么这样写是“资深”?
这段代码解决了一个 Next.js 开发中的巨大难题:灵活性。
如果你在 Page 里用:你可以用
dalVerifySuccess,报错了直接跳页面。如果你在 Server Action (表单提交) 里用:你可能不想跳页面,你想给用户显示个红色的文字提醒。这时你就不用
dalVerifySuccess,而是手动解构结果:xxxxxxxxxxconst res = await getTodos()if (!res.success) return { error: dalFormatErrorMessage(res.error) }解耦:这段代码让“获取数据”和“处理错误(跳转还是弹窗)”彻底分开了。
总结
dalRequireAuth和dalDbOperation是在定义数据函数时用的(保驾护航)。dalVerifySuccess等是在页面/组件里调用函数时用的(决定怎么对待结果)。
这是一个非常棒的问题,它触及了 dal-advanced 模式的核心价值:灵活性。
dal-advanced 并不强制你报错的方式,它只是给了你一份“标准化的诊断报告”,由你决定是在大屏幕上显示(Error 页面),还是弹一个小窗口(Toast)。
error.tsx (全屏报错)如果你希望 Next.js 捕捉到错误并跳转到错误页面,你会使用 dalThrowError 或 dalVerifySuccess。
只要一 throw,Next.js 就会自动激活最近的 error.tsx。
场景:加载一个至关重要的页面数据(如订单详情)。如果拿不到数据,页面没法渲染。
代码逻辑:
xxxxxxxxxx// page.tsx (Server Component)export default async function OrderPage() { const result = await getOrderDetails(id); // 如果 result.success 是 false,dalVerifySuccess 会抛出(throw)错误 // 只要一 throw,Next.js 就会自动激活最近的 error.tsx const data = dalVerifySuccess(result); return <OrderView data={data} />;}Toast (轻量提醒)如果你在 Server Action(比如用户点击“收藏”按钮)中使用,你通常不希望整个页面刷新报错,而是想在角落弹出一个 Toast。
场景:用户没登录就点收藏,或者数据库写操作失败。
代码逻辑:
xxxxxxxxxx// action.ts (Server Action)export async function toggleFavorite(id: string) { const result = await toggleFavoriteInDb(id); if (!result.success) { // 我们不 throw,而是返回一个包含错误信息的对象 return { errorMessage: dalFormatErrorMessage(result.error) }; } revalidatePath("/");}// client-component.tsxconst res = await toggleFavorite(id);if (res?.errorMessage) { toast.error(res.errorMessage); // 这里的 toast 就是页面上的小弹窗}redirect (自动跳转)这是 dal-advanced 提供的第三种路径。
dalLoginRedirect。它既不会弹 Toast,也不会让页面崩溃进入 error.tsx,而是直接把用户“瞬移”到 /login 页面。我们可以通过下面这张图来看清错误流转的过程:
| 处理方式 | 使用的函数 | 最终 UI 表现 | 适用场景 |
|---|---|---|---|
| 硬拦截 | dalVerifySuccess | error.tsx 页面 | 必须有数据才能渲染的页面组件。 |
| 软处理 | dalFormatErrorMessage | Toast 或 表单文字 | 按钮点击、表单提交等异步交互。 |
| 重定向 | dalLoginRedirect | 浏览器 URL 变化,跳转页面 | 鉴权失败、权限不足。 |
因为在没有这个系统之前,如果你在数据库函数里写死了 throw new Error(),你就只能用 error.tsx 报错,没法轻松地改成用 Toast 提示。
有了 dal-advanced,数据库函数只管吐出结果,显示方式的选择权交给了调用它的那行代码。
比如说这段代码:
xxxxxxxxxxexport async function getAllTodos() { // cacheTag("todos") return dalRequireAuth( () => dalDbOperation(() => { return db.query.TodoTable.findMany({ with: { user: true, }, }) }), { allowedRoles: ["admin"] }, )}已经使用dalDbOperation和dalRequireAuth保护好了操作,如果有报错,那么就会返回error数据,为什么还需要使用dalVerifySuccess来检查一遍呢?我的感觉就是代码重复了。

简单回答:因为如果报错了,dalDbOperation和dalRequireAuth只能返回错误对象,没有办法对错误进行UI方面的处理。而dalVerifySuccess可以依据相应的错误类型,对错误进行UI上的处理,比如说返回login页面、或者给出toast提示并返回上一页等等。
以下是Gemini给出的回答:
这是一个非常敏锐的观察!初看之下,确实感觉“保镖”和“医生”做了两遍检查。
但实际上,这正是 dal-advanced 模式最精妙的地方:一个是逻辑拦截,一个是UI决策。 它们并不重复,而是互补的。
以下是深度拆解,告诉你为什么这两行代码缺一不可:
dalRequireAuth(在 getAllTodos 内部)角色:底线保镖
{ success: false, error: { type: "no-access" } }。if (res.error.type === "no-access")角色:定制化 UI 策略
if,这实际上是在拦截掉这种特定的错误,不让它传给后面的 dalVerifySuccess。dalVerifySuccess角色:兜底清理员
任务:处理那些“剩下的、我没想好怎么处理”的所有错误。
执行逻辑:
redirect 去登录页)。throw 报错)。todos 数据(它不仅做检查,还起到了“解构数据”的作用)。请看这个逻辑分流过程:
如果你删掉其中一个会发生什么?
删掉 dalRequireAuth (内部保镖):
数据泄漏。任何人调用这个查询都能拿到所有用户的 Todo。这是安全事故。
删掉组件里的 if 判断:
当没权限时,dalVerifySuccess 会直接抛出一个硬错误(导致用户看到 error.tsx 的全屏报错页)。你就失去了展示那个漂亮的 <h1>You do not have access...</h1> 提示的机会。
删掉 dalVerifySuccess:
你拿到的 res 依然是一个包含 success, data, error 的复杂对象。你没法直接拿 todos 去做 reduce 操作,TypeScript 会因为你不确定 success 是否为真而报错。
总结:资深开发者的思维链条
查询层(DAL):总是用 dalRequireAuth 封装,保证安全性。
页面层(Page):
if 拦截掉。dalVerifySuccess。它会帮我把“没登录”的用户送走,把“报错”的用户交给错误页,最后顺手把干净的数据解构出来给我用。作者在这里展示的是:如何利用 dal-advanced 模式,既保留了默认的重定向/报错功能,又能灵活地为特定错误提供自定义的 UI 渲染。
https://www.youtube.com/watch?v=ovnyeq-Xxrc
视频中 Kyle (Web Dev Simplified) 按顺序演示了 5 个递进的错误处理版本,核心场景是 Next.js 项目(server actions / API routes)中处理登录、权限等错误(如 Unauthenticated、Unauthorized、ValidationError、Unexpected 等)。他使用了自定义错误类、权限检查、redirect vs JSON 响应等典型示例。
直接在 server action 或 API route 里写业务逻辑 + try-catch,处理不同错误类型(redirect 或 return error message)。
典型代码(重建自演示风格):
xxxxxxxxxx// app/login/action.ts (server action)'use server'export async function loginAction(formData: FormData) { try { const email = formData.get('email') as string; const password = formData.get('password') as string; // 业务逻辑 + 权限检查 if (!email || !password) throw new ValidationError('Email and password required'); const user = await db.login({ email, password }); if (!user) throw new UnauthenticatedError(); if (!user.hasPermission('admin')) throw new AuthorizationError(); redirect('/dashboard'); // success } catch (error) { if (error instanceof ValidationError) { return { error: error.message }; // for form } if (error instanceof UnauthenticatedError) { redirect('/login?error=unauthenticated'); } if (error instanceof AuthorizationError) { redirect('/unauthorized'); } return { error: 'Something went wrong' }; // unexpected }}哪里好:
哪里不好:
把核心业务 + 权限检查抽到独立 service 文件,service 只负责返回数据或 throw custom errors,不处理 redirect/JSON。
典型代码:
xxxxxxxxxx// services/authService.tsexport class ValidationError extends Error { }export class UnauthenticatedError extends Error { }export class AuthorizationError extends Error { }export async function loginService(credentials: {email: string; password: string}) { if (!credentials.email || !credentials.password) { throw new ValidationError('Email and password required'); } const user = await db.login(credentials); if (!user) throw new UnauthenticatedError(); if (!user.hasPermission('admin')) throw new AuthorizationError(); return user; // success}// app/login/action.tsexport async function loginAction(formData: FormData) { try { const user = await loginService({ /*...*/ }); redirect('/dashboard'); } catch (error) { // 仍然需要相同的 if instanceof 链处理 redirect / return if (error instanceof ValidationError) return { error: error.message }; if (error instanceof UnauthenticatedError) redirect('/login?error=unauthenticated'); // ... }}哪里好:
哪里不好:
Kyle 特别强调即使用了 service layer,问题依然存在:
好/不好:基本同上,只是更清晰地展示了为什么需要下一步。
不再 throw,而是返回 Result 类型(success 或 failure),用 discriminated union 让 TS 自动 narrowing。
典型代码(视频中使用 tuple 风格,简化版):
xxxxxxxxxx// types/result.tstype Result<S, E> = [S, null] | [null, E]; // tuple discriminated unionfunction ok<S, E = never>(data: S): Result<S, E> { return [data, null as never];}function error<S = never, E>(err: E): Result<S, E> { return [null as never, err];}// services/authService.tsexport async function loginService() : Promise<Result<User, ValidationError | UnauthenticatedError | AuthorizationError>> { if (!credentials.email ) return error(new ValidationError()); // ... if (!user) return error(new UnauthenticatedError()); if (!hasPermission) return error(new AuthorizationError()); return ok(user);}// actionconst result = await loginService();if (result[1] !== null) { // error case const err = result[1]; if (err instanceof ValidationError) return { error: err.message }; if (err instanceof UnauthenticatedError) redirect('/login?error=unauthenticated'); // ... // 用 satisfies never 强制 exhaustive: // const _exhaustive: never = err; // 如果漏掉分支,编译错误}const user = result[0]; // successredirect('/dashboard');哪里好:
satisfies never 可在编译时强制处理所有错误分支(exhaustive checking)。哪里不好:
{ok: true, value} 或 {ok: false, error})。
引入 neverthrow(轻量,无依赖),提供 Ok/Err、match、andThen、map 等方法,支持 AsyncResult。
典型代码:
xxxxxxxxxximport { ok, err, Result } from 'neverthrow';async function loginService(): Promise<Result<User, CustomErrorUnion>> { if () return err(new ValidationError()); // ... return ok(user);}const result = await loginService();result.match({ ok: (user) => redirect('/dashboard'), err: (error) => { if (error instanceof UnauthenticatedError) redirect('/login?...'); // exhaustive with satisfies }});// 链式示例result .andThen((user) => someNextOperation(user)) // 只在 ok 时执行 .match({ ok: , err: });还提到 ESLint 插件强制使用 Result(禁止直接 throw 或忽略错误)。
哪里好:
哪里不好:
总体:Kyle 的讲解非常清晰,从问题到方案递进强烈,推荐中高级 TS/Next.js 开发者观看。核心理念是“错误作为显式值 + 类型安全 + 架构分离”。没有专用 repo,代码是演示性质的。
在 Next.js 的 App Router(Server Components) 中,服务端发出的 fetch 请求不会出现在浏览器的 Network 面板里,这是目前(2025年底)框架的设计使然。但还是可以手动输出一些日志来调试。
1、在 next.config.js 里打开 fetch 日志(Next.js 内置,最方便)
xxxxxxxxxx/** @type {import('next').NextConfig} */const nextConfig = { logging: { fetches: { fullUrl: true, // 显示完整 url hmrRefreshes: true // 连热更新时缓存命中的请求也显示 } }}module.exports = nextConfig2、加上基本的错误捕获 + 丰富日志
xxxxxxxxxxexport default async function Home() { let data = null; let error = null; try { const start = Date.now(); const res = await fetch("https://....", { cache: "no-store" // 方便调试 }); const duration = Date.now() - start; console.log("[FETCH]", { url: res.url, status: res.status, ok: res.ok, time: `${duration}ms`, cacheStatus: res.headers.get("x-nextjs-cache") || "unknown" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); data = await res.json(); } catch (err) { error = err; console.error("[FETCH_ERROR]", { message: err.message, stack: err.stack?.split("\n").slice(0, 5), cause: err.cause }); } // ... 后面正常渲染}你会看到类似这样清晰的输出:
xxxxxxxxxx[FETCH] { url: 'https://random-word-api.herokuapp.com/word', status: 502, ok: false, time: '1243ms', cacheStatus: 'skip'}[FETCH_ERROR] { message: 'fetch failed', stack: [ ... ], cause: { code: 'ECONNRESET', ... }}